package com.bugsnag.ohos;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * Serialize an exception stacktrace and mark frames as "in-project"
 * where appropriate.
 *
 * @since 2021-07-01
 */
public class Stacktrace implements JsonStream.Streamable {
    private static final int STACKTRACE_TRIM_LENGTH = 200;

    private List<Stackframe> trace;

    /**
     * constructor
     *
     * @param frames
     */
    public Stacktrace(List<Stackframe> frames) {
        this.trace = limitTraceLength(frames);
    }

    /**
     * constructor
     *
     * @param stacktrace
     * @param projectPackages
     * @param logger
     */
    public Stacktrace(StackTraceElement[] stacktrace, Collection<String> projectPackages, Logger logger) {
        StackTraceElement[] frames = limitTraceLength(stacktrace);
        List<Stackframe> stackframes = new ArrayList<>();
        for (StackTraceElement frame : frames) {
            if (frame != null) {
                stackframes.add(serializeStackframe(frame, projectPackages, logger));
            }
        }
        trace = stackframes;
    }

    public List<Stackframe> getTrace() {
        return trace;
    }

    /**
     * Calculates whether a stackframe is 'in project' or not by checking its class against
     * [Configuration.getProjectPackages].
     * <p>
     * For example if the projectPackages included 'com.example', then
     * the `com.example.Foo` class would be considered in project, but `org.example.Bar` would
     * not.
     *
     * @param className
     * @param projectPackages
     * @return boolean
     */
    public static boolean inProject(String className, Collection<String> projectPackages) {
        for (String projectPackage : projectPackages) {
            if (className.startsWith(projectPackage)) {
                return true;
            }
        }
        return false;
    }

    private StackTraceElement[] limitTraceLength(StackTraceElement[] frames) {
        if (frames.length >= STACKTRACE_TRIM_LENGTH) {
            return Arrays.copyOfRange(frames, 0, STACKTRACE_TRIM_LENGTH);
        } else {
            return frames;
        }
    }

    private List<Stackframe> limitTraceLength(List<Stackframe> frames) {
        if (frames.size() >= STACKTRACE_TRIM_LENGTH) {
            return frames.subList(0, STACKTRACE_TRIM_LENGTH);
        } else {
            return frames;
        }
    }

    private Stackframe serializeStackframe(StackTraceElement el, Collection<String> projectPackages, Logger logger) {
        String className = el.getClassName();
        String methodName = el.getMethodName();
        if (!className.isEmpty()) {
            methodName = className + "." + el.getMethodName();
        }
        String file = el.getFileName() == null ? "Unknown" : el.getFileName();
        return new Stackframe(methodName, file, el.getLineNumber(),
            inProject(className, projectPackages), null, null);
    }

    @Override
    public void toStream(JsonStream writer) throws IOException {
        writer.beginArray();
        for (Stackframe stackframe : trace) {
            writer.value(stackframe);
        }
        writer.endArray();
    }
}
